package com.example.gateway;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Objects;
import java.util.TreeMap;

/**
 * @author karidyang
 */
@Service
@Slf4j
@RefreshScope
public class GpDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<GpDefineGatewayFilterFactory.GpConfig> {
    @Value("${signKey:abcdefg}")
    private String signKey;

    public GpDefineGatewayFilterFactory() {
        super(GpConfig.class);
    }

    @Override
    public GatewayFilter apply(GpConfig config) {
        return ((exchange, chain) -> {
            exchange.getAttributes().put("startTime", System.currentTimeMillis());
            HttpMethod method = exchange.getRequest().getMethod();
            StringBuilder params = new StringBuilder();
            if (method == HttpMethod.GET) {
                exchange.getAttributes().put("body", exchange.getRequest().getURI().getRawQuery());
                log.info("Pre Filter request, {} [ {} {} ] ", method, exchange.getRequest().getURI().getPath(), exchange.getRequest().getURI().getRawQuery());
                return returnMono(chain, exchange);
            } else {
                //重新构造request，参考ModifyRequestBodyGatewayFilterFactory
                ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());
                MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
                //重点
                Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
                    //因为约定了终端传参的格式，所以只考虑json的情况，如果是表单传参，请自行发挥
                    if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON_UTF8.isCompatibleWith(mediaType)) {
                        exchange.getAttributes().put("body", body);
                        JSONObject json = JSON.parseObject(body);
                        if (verifySign(json)) {
                            json.put("from", "gateway");
                            json.remove("sign");
                            return Mono.just(json.toJSONString());
                        } else {
                            return Mono.error(new SignException("签名错误"));
                        }
                    }
                    return Mono.empty();
                });
                BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
                HttpHeaders headers = new HttpHeaders();
                headers.putAll(exchange.getRequest().getHeaders());
                //猜测这个就是之前报400错误的元凶，之前修改了body但是没有重新写content length
                headers.remove("Content-Length");
                //MyCachedBodyOutputMessage 这个类完全就是CachedBodyOutputMessage，只不过CachedBodyOutputMessage不是公共的
                CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
                return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                    ServerHttpRequest decorator = this.decorate(exchange, headers, outputMessage);
                    return returnMono(chain, exchange.mutate().request(decorator).build());
                }));
            }
        });
    }

    private boolean verifySign(JSONObject json) {
        TreeMap<String, String> treeMap = new TreeMap<>();
        json.entrySet().stream().filter(entry -> !"sign".equals(entry.getKey())).forEach(entry -> treeMap.put(entry.getKey(), StringUtils.trimToEmpty(entry.getValue().toString())));
        String sign = json.getString("sign");
        String signStr = SignUtil.sing(signKey, treeMap);
        log.info("req sign={}, sign={}, {}", sign, signStr, signStr.equalsIgnoreCase(sign));
        return signStr.equalsIgnoreCase(sign);

    }

    private Mono<Void> returnMono(GatewayFilterChain chain, ServerWebExchange exchange){
        return chain.filter(exchange).then(Mono.fromRunnable(()->{
            Long startTime = exchange.getAttribute("startTime");
            if (startTime != null){
                long executeTime = (System.currentTimeMillis() - startTime);
                String body = exchange.getAttribute("body");
                int status = Objects.requireNonNull(exchange.getResponse().getStatusCode()).value();
                log.info(" {} {} {} {} {} ", exchange.getRequest().getMethodValue(), exchange.getRequest().getURI().getPath(), body, status, executeTime+"ms");
            }
        }));
    }
    ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0L) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set("Transfer-Encoding", "chunked");
                }
                return httpHeaders;
            }
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }

    public static class GpConfig {
    }
}
